Praca domowa nr 3 dotyczy metody wyjaśnień lokalnych przy użyciu metody Ceteris Paribus, która ocenia wpływ zmian wybranej zmiennej objaśniającej na zmiany predykcji modelu. Jest to więc metoda bardziej szczegółówa od poprzednich i w piramidzie eksploracji modelu znajduje się poziom niżej.
import pandas as pd
import numpy as np
import pickle
import dalex as dx
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.impute import SimpleImputer
import warnings
warnings.filterwarnings('ignore')
# Wczytanie i przygotowanie danych
full_data = pd.read_csv("data/hotel_bookings.csv")
full_data["agent"] = full_data["agent"].astype(str)
treshold = 0.005 * len(full_data)
agents_to_change = full_data['agent'].value_counts()[full_data['agent'].value_counts() < treshold].index
full_data.loc[full_data["agent"].isin(agents_to_change), "agent"] = "other"
countries_to_change = full_data['country'].value_counts()[full_data['country'].value_counts() < treshold].index
full_data.loc[full_data["country"].isin(countries_to_change), "country"] = "other"
# Określenie cech uwzględnionych w modelu
num_features = ["lead_time", "arrival_date_week_number",
"stays_in_weekend_nights", "stays_in_week_nights",
"adults", "previous_cancellations",
"previous_bookings_not_canceled",
"required_car_parking_spaces", "total_of_special_requests",
"adr", "booking_changes"]
cat_features = ["hotel", "market_segment", "country",
"reserved_room_type",
"customer_type", "agent"]
features = num_features + cat_features
# Podział na zmienne wyjaśniające i target
X = full_data.drop(["is_canceled"], axis=1)[features]
y = full_data["is_canceled"]
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, random_state=42)
W tej pracy domowej wykorzystuję model, który koduje zmienne kateogryczne, wykorzystując one hot encoding. Dzięki temu możemy bardziej intuicyjnie (w postaci wykresów słupkowych) przeglądać wygenerowane profile Ceteris Paribus dla zmiennych o charakterze kategorycznym danej.
Sam model wczytuję z pickle'a, a odpowiedniego notebooka dotyczącego modelowania można znaleźć już na repozytorium.
rf_model = pickle.load(open("RF_model", 'rb'))
predict_train = rf_model.predict(X_train)
predict_proba_train = rf_model.predict_proba(X_train)
explainer = dx.Explainer(rf_model, X_train, y_train, label = "RF classifier with OH encoding")
Weźmy pod uwagę 2300-tną obserwację w zbiorze treningowym. Spójrzmy, jak wyglądają wartości poszczególnych cech.
X_train.iloc[2300]
print(f"""Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: {predict_train[2300]},
\nPrawopodobieństwo przyporządkowania do targetu 1: {predict_proba_train[2300][1]},
\nPrawidłowa klasyfikacja to: {y_train.iloc[2300]}.""")
Dla wybranej obserwacji model przewiduje target = 1, co oznacza, że rezerwacja zostanie odwołana. Jest to rzeczywiście odpowiednia wartość - rezerwacja ta jest oznaczona w zbiorze jako odwołana.
cp1 = explainer.predict_profile(X_train.iloc[2300,:])
cp1.plot(title = "Profile CP dla zmiennych numerycznych")
cp1.plot(variable_type = "categorical", title = "Profile CP dla zmiennych kategorycznych")
0, a więc nieodwołania rezerwacji. Oprócz otrzymanej już wyżej dekompozycji, weźmiemy pod uwagę dekompozycję 4242-tnej obserwacji ze zbioru treningowego. Skupimy się na zmiennych numerycznych.
X_train.iloc[4242]
print(f"""Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: {predict_train[4242]},
\nPrawopodobieństwo przyporządkowania do targetu 1: {predict_proba_train[4242][1]},
\nPrawidłowa klasyfikacja to: {y_train.iloc[4242]}.""")
cp2 = explainer.predict_profile(X_train.iloc[4242,:])
cp2.plot(title = "Profile CP dla zmiennych numerycznych")
Możemy wskazać wiele cech, których profile znacząco się różnią dla zdekomponowanych obserwacji.
Dla przejrzystości przedstawmy je obok siebie. W lewej kolumnie znajdują się wykresy odpowiadające profilom dla pierwszej z analizowanych obserwacji (2300.), po prawej - dla drugiej (4242.).
Liczba dni poprzedzających termin rezerwacji lead_time:
Liczba dni roboczych, które obejmuje rezerwacja stays_in_week_nights:
Liczba wcześniejszych nieodwołanych rezerwacji klienta previous_bookings_not_canceled (obie obserwacje mają wartość 0):
Liczba zmian dotyczących rezerwacji booking_changes (obie obserwacje mają wartość 0):
W szczególności uwagę należy zwrócić na dwa ostatnie punkty, gdzie profile różnią się mimo takich samych wartości zmiennych. Pokazuje to interakcje w analizowanym modelu.

Różnice są duże natomiast należy zauwazyć, że obserwacje są z innych klas. Dlatego przygotuję jeszcze dekompozycje drugiej obserwacji zaklasyfikowanej jako 0 - rezerwacja nieodwołana.
X_train.iloc[1,:]
print(f"""Wyliczona predykcja dla wybranej obserwacji w zbiorze treningowym to: {predict_train[1]},
\nPrawopodobieństwo przyporządkowania do targetu 1: {predict_proba_train[1][1]},
\nPrawidłowa klasyfikacja to: {y_train.iloc[1]}.""")
cp3 = explainer.predict_profile(X_train.iloc[1,:])
cp3.plot(title = "Profile CP dla zmiennych numerycznych")
Również porównując obserwacje z tej samej klasy możemy znaleźć różnice w poszczególnych profilach.
Dla przejrzystości przedstawmy je znów obok siebie. W lewej kolumnie znajdują się wykresy odpowiadające profilom dla obserwacji 1. zdekomponowanej bezpośrednio powyżej, po prawej dla obserwacji 4242. zdekompowanej wcześniej.
Liczba zmian dotyczących rezerwacji booking_changes (obie obserwacje mają wartość 0):
Liczba wcześniejszych nieodwołanych rezerwacji klienta previous_bookings_not_canceled (obie obserwacje mają wartość 0):
Numer tygodnia roku, którego dotyczy rezerwacja arrival_date_week_number:
